﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;

namespace Close2Poco
{
    /// <summary>
    /// Finds relationships between classes
    /// </summary>
    public static class RelationshipFinder
    {
        readonly static RelationshipFinderImpl _instance = new RelationshipFinderImpl();  

        /// <summary>
        /// Given the property that forms one end of a relationship, this returns an object that describes the entire relationship
        /// </summary>
        public static Relationship GetRelationship(PropertyInfo oneEndOfRelationship)
        {
            return _instance.GetRelationship(oneEndOfRelationship);
        }

        public static bool IsEntityReference(PropertyInfo info)
        {
            return _instance.IsEntityReference(info);  //TODO: move?
        }

        public static bool IsEntityList(PropertyInfo info)
        {
            return _instance.IsEntityList(info);//TODO: move?
        }

        public static IList<PropertyInfo> GetPrimaryKeyFields(Type type)
        {
            //TODO: handle multi-key fields
            //TODO: Do we still want to keep the reflection-based fallback?
            PropertyInfo primaryKey = (from p in type.GetProperties()
                                       where (p.GetCustomAttributes(typeof(IsPrimaryKeyAttribute), true).Length > 0)
                                       select p).SingleOrDefault();
            if (primaryKey == null)
                primaryKey = type.GetProperty(type.Name + "ID") ?? type.GetProperty("ID");

            if (primaryKey == null)
                throw new ArgumentException("Could not find primary key in class " + type);

            return new List<PropertyInfo> { primaryKey };
        }

        public static IList<PropertyInfo> GetForeignKeyFields(PropertyInfo refProperty)
        {
            // TODO: this implementation is a bit of a hack, change it to:
            // (a) use the attributes that say what the "ThisKey" is (if such attributes are present) OR, by defafult
            // (b) make this kind of assumption here (i.e. our default will always have to be name-based)
            // Option (b) will only work for single-key fields.  (a) will have to be used in other cases
            // OR pass the ID to SetValue<EntityRef<T>... and use ActiveSharp to figure it out

            var assumedKeyName = refProperty.Name + "ID";
            PropertyInfo assumedKey = refProperty.DeclaringType.GetProperty(assumedKeyName);
            if(assumedKey == null)
                throw new NotSupportedException("Foreign key field name must match object property name. I.e. property named " + assumedKeyName + 
                    " is expected in class " + refProperty.DeclaringType + " to store the ID for property" + refProperty.Name);
            return new List<PropertyInfo> { assumedKey };
        }



        #region private implementation class

        /// <summary>
        /// Nested class to do the actual work. (This class can't be the singleton, since we don't want to hard code the 
        /// generic param into every call site, as would be necessary if this class itself was the singleton)
        /// </summary>
        /// <remarks>Note that it only finds relationsips between classes that are derived from <see cref="IClose2PocoEntity"/>
        /// That interface is just a marker interface - it has no members.  Have used interface rather than
        /// attribute so that <see cref="RelationshipHelper"/> can hang some extension methods off the interface 
        /// This code uses the terminology "list end" and "reference end" to refer to the two ends of the relationship.
        /// The list end has a property of type IList{T} (or something derived from IList{T}), whereas the
        /// the reference end is just a reference to a single object.</remarks>
        private class RelationshipFinderImpl
        {
            readonly Dictionary<PropertyInfo, Relationship> _byListEnds = new Dictionary<PropertyInfo, Relationship>();
            readonly Dictionary<PropertyInfo, Relationship> _byRefEnds = new Dictionary<PropertyInfo, Relationship>();
            readonly Dictionary<Type, object> _visitedClasses = new Dictionary<Type, object>();

            readonly ReaderWriterLockSlim _padlock = new ReaderWriterLockSlim();

            /// <summary>
            /// Given the property that forms one end of a relationship, this returns an object that describes the entire relationship
            /// </summary>
            /// <remarks>Since this method is the entire public API of the class (except for some helper methods), it is a good place to handle the locking</remarks>
            public Relationship GetRelationship(PropertyInfo oneEndOfRelationship)
            {
                Relationship result;
                using(new ReadLock(_padlock))
                {
                    result = InnerGet(oneEndOfRelationship);
                }

                if (result == null)
                {
                    using(new WriteLock(_padlock))
                    {
                        // the FindRelationps method updates shared data structures, so must only be called within a write lock
                        FindRelationships(oneEndOfRelationship.DeclaringType);
                        result = InnerGet(oneEndOfRelationship);                // try again, now that we have initialized
                    }
                }
                if (result == null)
                    throw new ArgumentException("Cannot find relationship for " + oneEndOfRelationship.Name + ". Do you need to inherit the class from " + typeof(IClose2PocoEntity).Name + "?");  // TODO: tidy up?
                return result;
            }

            Relationship InnerGet(PropertyInfo oneEndOfRelationship)
            {
                // look the result up in our two lists (since we don't know whether its a "list end" or a "reference end" of the relationship - we could figure that out, but instead we just check both lists)
                Relationship result;
                if (!_byListEnds.TryGetValue(oneEndOfRelationship, out result))
                    _byRefEnds.TryGetValue(oneEndOfRelationship, out result);
                return result;
            }

            //TODO: supprt cases where there are multiple links between the two classes..
            //TODO: think through and test impact of inheritance on this (subclasses of the related classes should not break anything, but do need to test)

            /// <summary>
            /// Initializes all the relationships for this particular class
            /// </summary>
            void FindRelationships(Type t)
            {
                if (_visitedClasses.ContainsKey(t))
                    return;                             // bail out if we have already done this one (no point in doing it again!)

                _visitedClasses[t] = null;
                // record that we have been here, buy putting type into dictionary as key (don't need a value)

                foreach (PropertyInfo info in t.GetProperties())
                {
                    Type typeInList;
                    if (IsEntityList(info, out typeInList))
                        InitializeFromListEnd(info, typeInList);
                    else if (IsEntityReference(info))
                        InitializeFromRefEnd(info);
                }
            }

            public bool IsEntityList(PropertyInfo info)
            {
                Type dummy;
                return IsEntityList(info, out dummy);
            }

            /// <summary>
            /// Is this property an <see cref="IList"/> of some type derived from <see cref="IClose2PocoEntity"/>?
            /// I.e. is it this "list end" of one of the relationships we are looking for?
            /// </summary>
            bool IsEntityList(PropertyInfo info, out Type typeInList)
            {
                Type propertyType = info.PropertyType;
                if (propertyType.IsGenericType)
                {
                    Type[] genericArguments = propertyType.GetGenericArguments();
                    if (genericArguments.Length == 1)
                    {
                        Type propertyGenericArg = genericArguments[0];
                        if (typeof(IClose2PocoEntity).IsAssignableFrom(propertyGenericArg))
                        {
                            // At this stage, we know that the property type has one generic type arguement, 
                            // and that that type derives from our DomainLayerSuperType, which is exactly what we want.
                            // E.g. the property type is: Something<T>, where T is derived from our DomainLayerSuperType
                            // What we don't know, yet, is whether that "Something" inherits from IList<>. 
                            // That's what we'll find out now...

                            // First, construct a hypothetical list type to test against.  (The only other way _might_ be to loop through all the property type's interfaces, as here: http://www.devnewsgroups.net/group/microsoft.public.dotnet.framework/topic26802.aspx
                            // E.g. if the property type is Something<Customer>, then 
                            // our hypothetical list type is IList<Customer>
                            Type hypotheticalListType = typeof(IList<>).MakeGenericType(propertyGenericArg);

                            // See if the property type inherits from our hypothetical list type
                            if (hypotheticalListType.IsAssignableFrom(propertyType))   // e.g. Yes, it is an IList<Customer>
                            {
                                typeInList = propertyGenericArg;
                                return true;
                            }
                        }
                    }
                }
                typeInList = null;
                return false;
            }

            /// <summary>
            /// Is this property a reference to a type derived from <see cref="IClose2PocoEntity"/>?
            /// I.e. is it the "reference end" of one of the relationships we are looking for
            /// </summary>
            public bool IsEntityReference(PropertyInfo info)
            {
                return typeof(IClose2PocoEntity).IsAssignableFrom(info.PropertyType);
            }

            /// <summary>
            /// Initialize a relationship, when all we have is information about the "list end" of the relationship
            /// Find the other end, and create and store the relationship information
            /// </summary>
            void InitializeFromListEnd(PropertyInfo listProperty, Type typeInList)
            {
                PropertyInfo refProperty = null;
                foreach (PropertyInfo testProp in typeInList.GetProperties())
                {
                    if (testProp.PropertyType == listProperty.DeclaringType)
                        if (refProperty == null)
                            refProperty = testProp;
                        else
                            throw new NotSupportedException("There are multiple links beween " + listProperty.DeclaringType + " and " + typeInList + ", which is not supported in this version of the code");
                }
                AddRelationship(listProperty, refProperty);
            }

            /// <summary>
            /// Initialize a relationship, when all we have is information about the "reference end" of the relationship.  
            /// Find the other end, and create and store the relationship information
            /// </summary>
            void InitializeFromRefEnd(PropertyInfo refProperty)
            {
                PropertyInfo listProperty = null;
                foreach (PropertyInfo testProp in refProperty.PropertyType.GetProperties())
                {
                    Type typeInList;
                    if (IsEntityList(testProp, out typeInList) && typeInList == refProperty.DeclaringType)
                        if (listProperty == null)
                            listProperty = testProp;
                        else
                            throw new NotSupportedException("There are multiple links beween " + refProperty.DeclaringType + " and " + typeInList + ", which is not supported in this version of the code");
                }
                AddRelationship(listProperty, refProperty);
            }

            /// <summary>
            /// Records a relationship between the two properties.
            /// </summary>
            void AddRelationship(PropertyInfo listProperty, PropertyInfo refProperty)
            {
                if (refProperty == null)
                    throw new NotSupportedException("In this version of this code, you cannot have relationship with a list at one end and NOTHING at the other.  Relationships involving a list or EntitySet MUST be bi-directional. Property was:  " + listProperty.DeclaringType + "." + listProperty.Name);
                // I.e. ref end is mandatory, but list end is OPTIONAL
                // Why, because the mapping generation requires the "thisKey" for the ref end (for BOTH ends of the mapping)
                // and the SetValue stuff for relationships also needs the thisKey for the ref end, even when there is NO list end

                IList<PropertyInfo> keyAtListEnd = listProperty == null ? null : GetPrimaryKeyFields(listProperty.DeclaringType);
                IList<PropertyInfo> keyAtRefEnd = GetForeignKeyFields(refProperty);

                // store the relationship in two dictionaries - one indexed by the "list end" property; and one by the "ref end" property
                Relationship r = new Relationship(listProperty, refProperty, keyAtListEnd, keyAtRefEnd);
                if(listProperty != null)
                    _byListEnds[listProperty] = r;   // only index this way if we do have a list end
                _byRefEnds[refProperty] = r;         // we ALWAYS have a ref end
            }

            
        }
        #endregion
    }
}